import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:f_cache/manager.dart';
import 'dart:ui' as ui;

/// The dart:io implementation of [ImageProvider].
@immutable
class CachedNetworkImage extends ImageProvider<CachedNetworkImage> {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
  const CachedNetworkImage(this.url, { this.scale = 1.0, this.headers, this.meta });

  /// Image [url]
  final String url;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  /// custom http [headers]
  final Map<String, String>? headers;

  final Map<String, dynamic>? meta;

  @override
  Future<CachedNetworkImage> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<CachedNetworkImage>(this);
  }


  @override
  ImageStreamCompleter loadImage(CachedNetworkImage key, ImageDecoderCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, chunkEvents, decode: decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: () => <DiagnosticsNode>[
        DiagnosticsProperty<ImageProvider>('Image provider', this),
        DiagnosticsProperty<CachedNetworkImage>('Image key', key),
      ],
    );
  }

  Future<ui.Codec> _loadAsync(
      CachedNetworkImage key,
      StreamController<ImageChunkEvent> chunkEvents, {
        required ImageDecoderCallback decode,
      }) async {
    try {
      assert(key == this);

      final cached = await CacheManager().getCachedFromUrl(
          url: url,
          onProcess: (c, a) {
            chunkEvents.add(ImageChunkEvent(
              cumulativeBytesLoaded: c,
              expectedTotalBytes: a,
            ));
          },
        headers: headers,
        meta: meta,
      );
      final bytes = cached.data;

      if (bytes.lengthInBytes == 0) {
        throw Exception('NetworkImage is an empty file: $url');
      }
      final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
      return decode(buffer);
    } catch (e) {
      // Depending on where the exception was thrown, the image cache may not
      // have had a chance to track the key in the cache at all.
      // Schedule a microtask to give the cache a chance to add the key.
      scheduleMicrotask(() {
        PaintingBinding.instance.imageCache.evict(key);
      });
      rethrow;
    } finally {
      chunkEvents.close();
    }
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is CachedNetworkImage
        && other.url == url
        && other.scale == scale;
  }

  @override
  int get hashCode => Object.hash(url, scale);

  @override
  String toString() => 'CachedNetworkImage("$url", scale: $scale)';
}
/// The dart:io implementation of [ImageProvider].
@immutable
class CachedReaderImage extends ImageProvider<CachedReaderImage> {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [id] and [scale] must not be null.
  const CachedReaderImage(this.id, this.readerBuilder, { this.scale = 1.0});

  /// returns data use cache if [id] is same, if no cache, read data from [reader]
  final String id;

  /// Image src data [reader]
  final ReaderBuilder readerBuilder;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  @override
  Future<CachedReaderImage> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<CachedReaderImage>(this);
  }


  @override
  ImageStreamCompleter loadImage(CachedReaderImage key, ImageDecoderCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, chunkEvents, decode: decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      debugLabel: key.id,
      informationCollector: () => <DiagnosticsNode>[
        DiagnosticsProperty<ImageProvider>('Image provider', this),
        DiagnosticsProperty<CachedReaderImage>('Image key', key),
      ],
    );
  }

  Future<ui.Codec> _loadAsync(
      CachedReaderImage key,
      StreamController<ImageChunkEvent> chunkEvents, {
        required ImageDecoderCallback decode,
      }) async {
    try {
      assert(key == this);

      final cached = await CacheManager().getCached(
        id: id,
        readerBuilder: readerBuilder,
        onProcess: (c, a) {
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: c,
            expectedTotalBytes: a,
          ));
        },
      );
      final bytes = cached.data;

      if (bytes.lengthInBytes == 0) {
        throw Exception('ReaderImage is an empty file: $id');
      }

      final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
      return decode(buffer);
    } catch (e) {
      // Depending on where the exception was thrown, the image cache may not
      // have had a chance to track the key in the cache at all.
      // Schedule a microtask to give the cache a chance to add the key.
      scheduleMicrotask(() {
        PaintingBinding.instance.imageCache.evict(key);
      });
      rethrow;
    } finally {
      chunkEvents.close();
    }
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is CachedReaderImage
        && other.id == id
        && other.scale == scale;
  }

  @override
  int get hashCode => Object.hash(id, scale);

  @override
  String toString() => 'CachedReaderImage("$id", scale: $scale)';
}
